require 'net/telnet'

class SDTD_Server
  #
  attr_reader :server, :port, :connected
  #
  def whitelist
    placeholder
  end
  #
  def admin
    placeholder
  end
  #
  def commandpermission
    placeholder
  end
  #
  def cp
    commandpermission
  end
  #
  def weathersurvival(correct_state = nil)
    raise InvalidFunctionInputError unless ['on', 'off', nil].include? correct_state

    command = correct_state == nil ? 'weathersurvival' : "weathersurvival #{correct_state}"

    console_output = telnet_command command, /Weather survival/, /Weather survival is (on|off)/

    return correct_state == nil ? console_output.first : nil
  end
  #
  def spawnwanderinghorde
    # I don't think this really does anything...
    telnet_command 'spawnwanderinghorde'
    return nil
  end
  #
  def spawnentity(player_id, ent_id)
    spawn_results = telnet_command "spawnentity #{player_id.to_s} #{ent_id.to_s}", /^No spawn point|^Spawned|^Player with|Entity/

    return nil if spawn_results.include? 'Spawned'

    if spawn_results.include? 'No spawn point'
      raise NoSpawnPointError
    elsif spawn_results.include? 'Player with id'
      raise InvalidSpawnUserError
    elsif spawn_results.include? 'Entity'
      raise InvalidEntIDError
    else
      # This should never happen as telnet will raise a timeout error...
      raise UnexpectedResultsError
    end
  end
  #
  def se(player_id, ent_id)
    spawnentity player_id, ent_id
  end
  #
  def spawnairdrop
    telnet_command 'spawnairdrop'
    return nil
  end
  #
  def loggamestate(message)
    telnet_command "loggamestate \"#{message}\"", /Wrote game state/
    return nil
  end
  #
  def lgs(message)
    loggamestate message
  end
  #
  def listplayers
    # Regex Exampple: http://regexr.com/3cgoe
    begin
      player_list_output = telnet_command 'listplayers', /in the game/, /\d+\.\sid=(\d+),\s([^,]+), pos=\(([^\)]+)\), rot=\(([^\)]+)\), remote=(True|False), health=(\d+), deaths=(\d+), zombies=(\d+), players=(\d+), score=(\d+), level=(\d+), steamid=(\d+), ip=([^,]+), ping=(\d+)/
    rescue NoMethodError
      return {}
    end

    player_data = Hash.new

    player_keys = [
      'name', 'position', 'rotation',
      'is_remote', 'health', 'deaths',
      'zombies', 'players', 'score',
      'level', 'steamid', 'ip',
      'ping']

    player_list_output.each do |player|

      player_id = player.shift
      player_data[player_id] = Hash.new

      player_keys.each do |key|
        player_data[player_id][key.to_sym] = player.shift
      end
    end

    return player_data
  end
  #
  def lp
    listplayers
  end
  #
  def listents
    # Regex Example: http://regexr.com/3cgok
    listents_results = telnet_command 'listents', /in the game/, /\d\. id=(\d+), \[type=(\w+), name=(\w+), id=\d+\], pos=\(([^\)]+)\), rot=\(([^\)]+)\), lifetime=([^,]+), remote=(True|False), dead=(True|False), health=(\d+)/

    ent_keys = [
      'type', 'name', 'position',
      'rotation', 'lifetime', 'remote',
      'dead', 'health']

    ents = Hash.new

    listents_results.each do |ent|

      ent_id = ent.shift.to_sym
      ents[ent_id] = Hash.new

      ent_keys.each do |key|
        ents[ent_id][key.to_sym] = ent.shift
      end
    end

    return ents
  end
  #
  def le
    listents
  end
  #
  def listgameobjects
    return telnet_command('listgameobjects', /took/, /GOs: (\d+),/).first
  end
  #
  def lgo
    listgameobjects
  end
  #
  def killall
    telnet_command 'killall'
    return nil
  end
  #
  def kill(ent_id)
    kill_results = telnet_command "kill #{ent_id.to_s}", /not found\.|damage to entity/

    raise InvalidEntIDError unless kill_results.include? 'damage to'

    return nil
  end
  #
  def kick(user, reason=nil)
    user = user.to_s

    kick_command = reason == nil ? "kick #{user}" : "kick #{user} \"#{reason}\""
    kick_results = telnet_command kick_command, /not a valid|Kicking/

    raise InvalidUserError unless kick_results.include? 'Kicking'
    return nil
  end
  #
  def ban(subcommand, user_id=nil, time=nil, units=nil, reason=nil)
    case subcommand
      when 'list'
        return ban_list
      when 'remove'
        raise MissingCommandOptionError unless user_id
        return ban_remove user_id
      when 'add'
        raise MissingCommandOptionError unless user_id and time and units
        return ban_add user_id, time, units, reason
    else
      raise UnknownBanSubCommandError
    end

    return nil
  end
  #
  def getgamepref(pref_name=nil)
    return get_all_game_prefs unless pref_name

    gamepref = telnet_command "getgamepref #{pref_name}", /GamePref\./, /GamePref\.[^=]+=\s+(.*)/
    gamepref = gamepref.first

    return gamepref.first
  end
  #
  def gg(pref_name=nil)
    getgamepref pref_name
  end
  #
  def version
    # Regex example: http://regexr.com/3cgo5
    version_output = telnet_command 'version', /version/, /Game version:\s+(.+)Compatibility Version:\s+(.+)$/
    version_output = version_output.first

    return {:version => version_output.first, :compatibility_version => version_output.last}
  end
  #
  def shutdown
    kickall 'The server is shutting down soon...'
    telnet_command 'shutdown'
    return nil
  end
  #
  def say(message)
    telnet_command "say \"#{message}\""
    return nil
  end
  #
  def kickall(reason="Everyone has been kicked from the server.")
    telnet_command "kickall \"#{reason}\""
    return nil
  end
  #
  def setgamepref(pref_name, new_value)
    set_game_pref_output = telnet_command "setgamepref #{pref_name} #{new_value}", /set|Error parsing parameter/

    raise UnknownGamePrefError if set_game_pref_output.include? 'Error parsing parameter'

    return nil
  end
  #
  def sg(pref_name, new_value)
    setgamepref pref_name, new_value
  end
  #
  def settime(params = {})
    # TODO: Add the ability to accept only 1 or 3 inputs to the function, just like the console command.
    day    = params.fetch(:day, '1')
    hour   = params.fetch(:hour, '08')
    minute = params.fetch(:minute, '00')

    telnet_command "settime #{day} #{hour} #{minute}", /Set time to/
  end
  #
  def st(params = {})
    settime params
  end
  #
  def gettime
    get_time_results = telnet_command 'gettime', /Day/, /Day\s+(\d+),\s+(\d+):(\d+)/
    get_time_results = get_time_results.first

    return {:day => get_time_results.shift, :hour => get_time_results.shift, :minute => get_time_results.shift}
  end
  #
  def gt
    gettime
  end
  #
  def saveworld
    telnet_command 'saveworld', /World saved/
    return nil
  end
  #
  def sa
    saveworld
  end
  #
  def mem
    # Regex extract example: http://regexr.com/3cglt
    mem_results = telnet_command 'mem', /Time:/, /Time:\s+([^\s]+)\s+FPS:\s+([\d\.]+)\s+Heap:\s+([^\s]+)\s+Max:\s+([^\s]+)\s+Chunks:\s+(\d+)\s+CGO:\s+(\d+)\s+Ply:\s+(\d+)\s+Zom:\s+(\d+)\s+Ent:\s+([^\)]+\))\s+Items:\s+(\d+)\s+CO:\s+(\d+)/
    mem_results = mem_results.first

    keys = [
      'time', 'fps', 'heap',
      'max', 'chunks', 'cgo',
      'ply', 'zom', 'ent',
      'items', 'co'
    ]

    result_hash = Hash.new

    keys.each { |key| result_hash[key]=mem_results.shift }

    return result_hash
  end
  #
  def listplayerids
    console_output = telnet_command 'listplayerids', /Total of/

    players = Hash.new

    console_output.scan( /\d+\.\s+id=(\d+),\s+(.+)/ ).each do |current_player|
      user_id   = current_player[0].to_sym
      user_name = current_player[1]

      players[user_id] = user_name
    end

    return players
  end
  #
  def lpi
    listplayerids
  end
  #
  def close
    close_telnet_connection
  end
  #
  private
  def ban_add(user_id, time, units, reason)
    ban_add_command = "ban add #{user_id} #{time} #{units}"
    ban_add_command += " \"#{reason}\"" unless reason == nil

    ban_add_results = telnet_command ban_add_command, /banned|not a valid entity id|valid integer|duration unit/

    return nil if ban_add_results.include? 'banned'

    if ban_add_results.include? 'not a valid entity id'
      raise InvalidUserError
    elsif ban_add_results.include? 'valid integer'
      raise InvalidBanTimeError
    elsif ban_add_results.include? 'duration unit'
      raise InvalidBanUnitError
    else
      raise UnexpectedResultsError
    end

    return nil
  end
  #
  def ban_remove(steam_id)
    remove_results = telnet_command "ban remove #{steam_id}", /not a valid steam id\.|removed/

    return nil if remove_results.include? 'removed from'

    if remove_results.include? 'is not a valid steam id.'
      raise InvalidUserError
    end

    return nil
  end
  #
  def ban_list
    # Regex Example: http://regexr.com/3cgp3
    ban_data = telnet_command 'ban list', /-/, /\s([\d\/]{8,10})\s([\d:]+\s(?:AM|PM))\s-\s(\d+)\s-\s?(.*)/

    return {} unless ban_data.length > 0

    ban_keys = ['expire_date', 'expire_time', 'reason']

    bans = Hash.new

    ban_data.each do |this_ban|
      player_id = this_ban.delete_at 2

      bans[player_id] = Hash.new

      ban_keys.each { |key| bans[player_id][key.to_sym] = this_ban.shift }
    end

    return bans
  end
  #
  def get_all_game_prefs
    game_pref_results = telnet_command('getgamepref', /^GamePref\.ZombiesRun/).split "\n"

    game_prefs = Hash.new

    game_pref_data = game_pref_results.select{ |x| x if x.include? 'GamePref.' }.map do |current_pref|
      extracted_pref_data = current_pref.match( /^GamePref\.([^\s]+)\s+=\s+(.*)/ ).captures

      key   = extracted_pref_data.first.to_sym
      value = extracted_pref_data.last

      game_prefs[key] = value
    end

    return game_prefs
  end
  #
  def loglevel(level, state)
    level.upcase!

    valid_log_levels = [
      'INF', 'WRN', 'ERR',
      'EXC', 'ALL']

    raise InvalidConsoleLogLevelError unless valid_log_levels.include? level and [true, false].include? state

    results = telnet_command "loglevel #{level} #{state}", /on this connection\./
  end
  #
  def console_login
    login_response = telnet_command @password, /Logon successful.|Password incorrect/
    raise InvalidConsolePasswordError unless login_response.include? 'Logon successful.'
    @connected = true
  end
  #
  def close_telnet_connection
    @connection.cmd('exit')
    @connection.close
  end
  #
  def telnet_command(string, match=/.*/, extract=nil)
    command_results = @connection.cmd('String' => string, 'Match' => match)
    return command_results unless extract
    return command_results.scan(extract)
  end
  #
  def initialize(params = {})
    #
    @server     = params.fetch(:server, 'localhost')
    @port       = params.fetch(:port, '8081')
    @password   = params.fetch(:password, 'CHANGEME')
    @connection = Net::Telnet.new('Host' => @server, 'Port' => @port, 'Telnetmode' => false)
    @connected  = false
    #
    console_login
    loglevel 'ALL', false
    #
  end
  #
  def placeholder
    puts "This function has not been added yet."
  end
end

class InvalidBanTimeError < StandardError ; end
class InvalidBanUnitError < StandardError ; end
class InvalidConsoleLogLevelError < StandardError ; end
class InvalidConsolePasswordError < StandardError ; end
class InvalidEntIDError < StandardError ; end
class InvalidFunctionInputError < StandardError ; end
class InvalidSpawnUserError < StandardError ; end
class InvalidUserError < StandardError ; end
class MissingCommandOptionError < StandardError ; end
class NoSpawnPointError < StandardError ; end
class UnexpectedResultsError < StandardError ; end
class UnknownBanSubCommandError < StandardError ; end
class UnknownGamePrefError < StandardError ; end
